1 /*
2 Copyright: Marcelo S. N. Mancini (Hipreme|MrcSnm), 2018 - 2021
3 License:   [https://creativecommons.org/licenses/by/4.0/|CC BY-4.0 License].
4 Authors: Marcelo S. N. Mancini
5 
6 	Copyright Marcelo S. N. Mancini 2018 - 2021.
7 Distributed under the CC BY-4.0 License.
8    (See accompanying file LICENSE.txt or copy at
9 	https://creativecommons.org/licenses/by/4.0/
10 */
11 
12 module hip.systems.compilewatcher;
13 
14 version(Load_DScript):
15 import fswatch;
16 import std.concurrency;
17 import hip.util.path;
18 import std.datetime.stopwatch;
19 import core.time:Duration,dur;
20 import hip.util.system;
21 
22 pragma(inline, true) private bool hasExtension(string file, ref immutable(string[]) extensions)
23 {
24     file = extension(file);
25     foreach(e;extensions) if(file == e) return true;
26     return false;
27 }
28 
29 
30 //Don't wait at all
31 private __gshared Duration timeout = dur!"msecs"(30);//Saves a lot of CPU Time
32 
33 enum WatchFSDelay = 250;
34 
35 void watchFs(Tid tid, string watchDir,
36 immutable(string[]) acceptedExtensions, immutable(string[]) ignoreDirs)
37 {
38     import core.thread.osthread:Thread;
39     bool shouldWatchFS = true;
40     FileWatch watcher = FileWatch(watchDir, true);
41     auto stopwatch = StopWatch(AutoStart.yes);
42     long lastTime = stopwatch.peek.total!"msecs";
43     string lastEventPath;
44     while (shouldWatchFS)
45 	{
46         receiveTimeout(timeout, 
47         (bool exit) //The data is not important at all
48         {
49             shouldWatchFS = false;
50         });
51 		foreach (event; watcher.getEvents())
52 		{
53             // if (event.type == FileChangeEventType.create) Although creation is important, it only makes sense
54             //When it is imported by any module, so, modify only
55             if (event.type == FileChangeEventType.modify)
56             {
57                 if(hasExtension(event.path,acceptedExtensions))
58                 {
59                     lastTime = stopwatch.peek.total!"msecs";
60                     lastEventPath = event.path;
61                 }
62                 // send(tid, event.path);
63             }
64             // else if (event.type == FileChangeEventType.remove) Remove should not trigger compilation
65         }
66         if(lastEventPath && stopwatch.peek.total!"msecs" - lastTime > WatchFSDelay)
67         {
68             send(tid, lastEventPath);
69             lastEventPath = null;
70         }
71         // if (event.type == FileChangeEventType.rename) (Rename should not compile, it is not important)
72         // else if (event.type == FileChangeEventType.createSelf) The folder should not be created while watching
73         // else if (event.type == FileChangeEventType.removeSelf) It should not be removed while watching
74 
75     }
76     stopwatch.stop();
77     send(tid, true);
78 }
79 
80 
81 ///Use these property and function for not allocating closures everytime
82 private __gshared string compilerWatcherFileUpdate;
83 private void checkFileWatcher(string theFile)
84 {
85     compilerWatcherFileUpdate = theFile;
86 }
87 
88 class CompileWatcher
89 {
90     string watchDir;
91     string[] acceptedExtensions;
92     string[] ignoredDirs;
93     void function(string fileName) handler;
94     Tid worker;
95 
96     bool isRunning = false;
97 
98     this(string watchDir, void function(string fileName) handler = null, 
99     string[] acceptedExtensions = [], string[] ignoredDirs = [])
100     {
101         this.watchDir = watchDir;
102         foreach(ext; acceptedExtensions)
103         {
104             if(ext[0] != '.')
105                 this.acceptedExtensions~= '.' ~ ext;
106             else
107                 this.acceptedExtensions~=ext;
108         }
109         this.ignoredDirs = ignoredDirs;
110         this.handler = handler;
111     }
112 
113     CompileWatcher run()
114     {
115         assert(!isRunning,  "CompileWatcher is already running");
116         // assert(handler != null, "CompileWatcher must have some handler before running");
117         isRunning = true;
118         worker = spawn(&watchFs, thisTid, watchDir, 
119         acceptedExtensions.idup, ignoredDirs.idup);
120         return this;
121     }
122     void stop()
123     {
124         if(isRunning)
125             send(worker, true);
126     }
127 
128     string update()
129     {
130         if(isRunning)
131         {
132             receiveTimeout(timeout, &checkFileWatcher);
133         }
134         return compilerWatcherFileUpdate;
135     }
136 
137     ~this()
138     {
139         stop();
140     }
141 }